package cmdutil

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/Songmu/timeout"
	log "github.com/sirupsen/logrus"
	"io"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"
)

// 默认使用sh执行命令行，如果是windows系统，就使用cmd执行
// exec.Command 不通过 shell 执行命令，它直接启动外部进程，依赖操作系统的路径查找方式。临时设置的ENV 不生效
// cmd.exe /C 通过 shell 解析命令和环境变量，能够正确地根据 PATH 查找命令,更加接近于原生cli的调用。
var cmdBase = []string{"sh", "-c"}

func init() {
	if runtime.GOOS == "windows" {
		cmdBase = []string{"cmd", "/U", "/c"}
	}
}

var errTimedOut = errors.New("sub timed out")

// CommandOption 命令行执行时需要考虑的细节
type CommandOption struct {
	User            string        // 使用什么身份执行，
	Env             string        // 使用哪些环境变量
	TimeoutDuration time.Duration // 命令执行超时时间
}

// 命令行执行的默认超时时间和kill时间
var (
	defaultTimeoutDuration = 30 * time.Second
	timeoutKillAfter       = 10 * time.Second
)

// RunCommandArgs 直接执行命令行
func RunCommandArgs(cmdArgs []string, opt CommandOption) (stdout, stderr string, err error) {
	args := append(cmdBase, cmdArgs...)
	// 判断命令行是否需要使用特定账户运行
	if opt.User != "" {
		if runtime.GOOS != "windows" {
			args = append([]string{"sudo", "-Eu", opt.User}, args...)
		}
	}
	// 第一个参数是需要执行的命令或者插件执行文件，后面的都是参数
	var cmd *exec.Cmd
	cmd = exec.Command(args[0], args[1:]...)
	// 设置环境变量
	if opt.Env != "" {
		cmd.Env = append(os.Environ(), fmt.Sprintf("PATH=%s;%s", opt.Env, os.Getenv("PATH")))
	}
	outbuf := &bytes.Buffer{}
	errbuf := &bytes.Buffer{}
	cmd.Stdout = outbuf
	cmd.Stderr = errbuf
	tio := &timeout.Timeout{
		Cmd:       cmd,
		Duration:  defaultTimeoutDuration,
		KillAfter: timeoutKillAfter,
	}
	// 如果指定了超时时间，就使用指定的超时时间覆盖默认的超时时间
	if opt.TimeoutDuration != 0 {
		tio.Duration = opt.TimeoutDuration
	}
	exitStatus, err := tio.RunContext(context.Background())
	if err != nil {
		return
	}
	stdout = decodeBytes(outbuf)
	stderr = decodeBytes(errbuf)
	// 如果exec执行是被 Songmu/timeout 包中的超时时间中断的，此时的err=nil，需要对 exitCode 和 err 重新赋值
	if exitStatus.IsTimedOut() {
		// 超时调用时有可能出现 执行成功，但是判定超时的情况，此时检查如果没有错误，并且输出结果是json格式的，就说明没有执行错误
		if strings.TrimSpace(stderr) == "" && json.Valid([]byte(stdout)) {
			err = nil
		} else {
			err = errTimedOut
		}
		return
	}
	if stderr != "" {
		err = errors.New(stderr)
		return
	}
	// 说明执行成功，返回直接结果， err=nil
	return
}
func RunCommandArgsForPipe(cmdArgs []string, opt CommandOption, out chan string, clo chan error, cancel context.Context) {
	args := append(cmdBase, cmdArgs...)
	// 判断命令行是否需要使用特定账户运行
	if opt.User != "" {
		if runtime.GOOS == "windows" {
			log.Warnf("RunCommand ignore option: user = %q", opt.User)
		} else {
			args = append([]string{"sudo", "-Eu", opt.User}, args...)
		}
	}
	var cmd = exec.CommandContext(cancel, args[0], args[1:]...)
	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		log.Errorf("RunCommand ignore option: user = %q", opt.User)
		clo <- err
	}
	var bufReader = bufio.NewReader(stdoutPipe)
	go func() {
		var buffer = make([]byte, 1024)
		for {
			n, err := bufReader.Read(buffer)
			if err != nil {
				stdoutPipe.Close()
				if !errors.Is(err, io.EOF) {
					clo <- err
				}
				return
			}
			out <- string(buffer[:n])
		}
	}()
	if err = cmd.Run(); err != nil {
		_ = stdoutPipe.Close()
		if err.Error() != "exit status 1" {
			log.Errorf("执行命令时发生错误:%s", err)
			clo <- err
		}
	}
}
